diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/MarkTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/MarkTest.kt
index a838488a0..5c64be685 100755
--- a/src/test/java/org/jetbrains/plugins/ideavim/action/MarkTest.kt
+++ b/src/test/java/org/jetbrains/plugins/ideavim/action/MarkTest.kt
@@ -271,6 +271,186 @@ class MarkTest : VimTestCase() {
     assertOffset(14)
   }
 
+  // VIM-3731 |m| |[`|
+  @Test
+  fun testGotoPreviousMark() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "[`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(14)
+  }
+
+  // VIM-3731 |m| |[`|
+  @Test
+  fun testGotoPreviousMarkIgnoresPlacingOrder() {
+    typeTextInFile(
+      injector.parser.parseKeys("mb" + "kma" + "jwmc" + "[`"),
+      """
+      one two
+      three
+      <caret>four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(14)
+  }
+
+  // VIM-3731 |m| |[`|
+  @Test
+  fun testGotoPreviousMarkMultipleMarksOnSamePosition() {
+    typeTextInFile(
+      injector.parser.parseKeys("mb" + "kma" + "jwmcmd" + "[`"),
+      """
+      one two
+      three
+      <caret>four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(14)
+  }
+
+  // VIM-3731 |m| |[`|
+  @Test
+  fun testGotoPreviousMarkWithCount() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "2[`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(8)
+  }
+
+  // VIM-3731 |m| |[`|
+  @Test
+  fun testGotoPreviousMarkWithExcessiveCount() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "5[`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(8)
+  }
+
+  // VIM-3731 |m| |[`|
+  @Test
+  fun testGotoPreviousMarkBeforeFirstMarkDoesNothing() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "ggw"+ "[`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(4)
+  }
+
+  // VIM-3731 |m| |]`|
+  @Test
+  fun testGotoNextMark() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "gg" + "]`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(8)
+  }
+
+  // VIM-3731 |m| |]`|
+  @Test
+  fun testGotoNextMarkIgnoresPlacingOrder() {
+    typeTextInFile(
+      injector.parser.parseKeys("mb" + "kma" + "jwmc" + "gg" + "]`"),
+      """
+      one two
+      three
+      <caret>four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(8)
+  }
+
+  // VIM-3731 |m| |]`|
+  @Test
+  fun testGotoNextMarkMultipleMarksOnSamePosition() {
+    typeTextInFile(
+      injector.parser.parseKeys("mbmd" + "kma" + "jwmc" + "ggjj" + "]`"),
+      """
+      one two
+      three
+      <caret>four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(19)
+  }
+
+  // VIM-3731 |m| |]`|
+  @Test
+  fun testGotoNextMarkWithCount() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "gg" + "2]`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(14)
+  }
+
+  // VIM-3731 |m| |]`|
+  @Test
+  fun testGotoNextMarkWithExcessiveCount() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "gg" + "5]`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(19)
+  }
+
+  // VIM-3731 |m| |]`|
+  @Test
+  fun testGotoNextMarkAfterLastMarkDoesNothing() {
+    typeTextInFile(
+      injector.parser.parseKeys("ma" + "jmb" + "wmc" + "ll"+ "]`"),
+      """
+      one two
+      <caret>three
+      four five
+     
+      """.trimIndent(),
+    )
+    assertOffset(21)
+  }
+
   // |i| |`]|
   @Test
   fun testGotoLastChangePositionEnd() {
@@ -543,4 +723,67 @@ class MarkTest : VimTestCase() {
       """.trimIndent(),
     )
   }
+
+  @Test
+  fun testMulticaretPreviousNextMark() {
+    configureByText(
+      """
+    My mother <caret>taught me this trick:
+    if you repeat something <caret>over and over again it loses its meaning.
+    For example: homework, homework, homework, homework, <caret>homework, homework, homework, homework, homework.
+    See, nothing.
+    
+      """.trimIndent(),
+    )
+    typeText("mawmbw")
+    assertState(
+      """
+    My mother taught me <caret>this trick:
+    if you repeat something over and <caret>over again it loses its meaning.
+    For example: homework, homework, homework, homework, homework, <caret>homework, homework, homework, homework.
+    See, nothing.
+    
+      """.trimIndent(),
+    )
+    typeText("[`")
+    assertState(
+      """
+    My mother taught <caret>me this trick:
+    if you repeat something over <caret>and over again it loses its meaning.
+    For example: homework, homework, homework, homework, homework<caret>, homework, homework, homework, homework.
+    See, nothing.
+    
+      """.trimIndent(),
+    )
+    typeText("[`")
+    assertState(
+      """
+    My mother <caret>taught me this trick:
+    if you repeat something <caret>over and over again it loses its meaning.
+    For example: homework, homework, homework, homework, <caret>homework, homework, homework, homework, homework.
+    See, nothing.
+    
+      """.trimIndent(),
+    )
+    typeText("[`") // Does nothing on first mark.
+    assertState(
+      """
+    My mother <caret>taught me this trick:
+    if you repeat something <caret>over and over again it loses its meaning.
+    For example: homework, homework, homework, homework, <caret>homework, homework, homework, homework, homework.
+    See, nothing.
+    
+      """.trimIndent(),
+    )
+    typeText("5]`") // Excessive count goes to last mark.
+    assertState(
+      """
+    My mother taught <caret>me this trick:
+    if you repeat something over <caret>and over again it loses its meaning.
+    For example: homework, homework, homework, homework, homework<caret>, homework, homework, homework, homework.
+    See, nothing.
+    
+      """.trimIndent(),
+    )
+  }
 }
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt
index 1b6d4d8d3..f7768d940 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt
@@ -63,3 +63,26 @@ class MotionGotoMarkNoSaveJumpAction : MotionActionHandler.ForEachCaret() {
     return injector.motion.moveCaretToMark(caret, mark, false)
   }
 }
+
+@CommandOrMotion(keys = ["]`"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
+class MotionGotoNextMarkAction: MotionGotoRelativeMarkAction(countMultiplier = 1) {
+}
+
+@CommandOrMotion(keys = ["[`"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
+class MotionGotoPreviousMarkAction: MotionGotoRelativeMarkAction(countMultiplier = -1) {
+}
+
+sealed class MotionGotoRelativeMarkAction(private val countMultiplier: Int) : MotionActionHandler.ForEachCaret() {
+  override val motionType: MotionType = MotionType.EXCLUSIVE
+
+  override fun getOffset(
+    editor: VimEditor,
+    caret: ImmutableVimCaret,
+    context: ExecutionContext,
+    argument: Argument?,
+    operatorArguments: OperatorArguments,
+  ): Motion {
+    return injector.motion.moveCaretToMarkRelative(caret, operatorArguments.count1 * countMultiplier)
+  }
+
+}
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkService.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkService.kt
index 5d6c35402..78ea497a0 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkService.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkService.kt
@@ -45,6 +45,11 @@ interface VimMarkService {
    */
   fun getMark(caret: ImmutableVimCaret, char: Char): Mark?
 
+  /**
+   * Get previous / next lowercase mark for specified caret
+   */
+  fun getRelativeLowercaseMark(caret: ImmutableVimCaret, count: Int): Mark?
+
   /**
    * Gets all marks for caret
    */
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt
index 24634c47f..30f8a44ef 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt
@@ -121,6 +121,53 @@ abstract class VimMarkServiceBase : VimMarkService {
     }
   }
 
+  override fun getRelativeLowercaseMark(caret: ImmutableVimCaret, count: Int): Mark? {
+    val path = caret.editor.getPath() ?: return null
+    if (count == 0) return null
+    val marks = if (caret.isPrimary) {
+      getLocalMarks(path).values
+    } else {
+      caret.markStorage.getMarks().values.toSet()
+    }
+    val lowerCaseMarksWithDistinctPositions = marks.filter { LOWERCASE_MARKS.contains(it.key) }
+      .sortedWithAndDistinctBy(Mark.PositionSorter)
+    // Use a fake mark to easily find the position of the caret in the list of marks.
+    val caretMark = createMark(caret, '[', caret.offset) ?: return null
+    val result = lowerCaseMarksWithDistinctPositions.binarySearch(caretMark, Mark.PositionSorter)
+    val targetIndex = if (result >= 0) {
+      // Caret is on a mark.
+      result + count
+    } else {
+      val insertionPoint = -1 * (result + 1)
+      if ((insertionPoint == 0 && count < 0)
+        || (insertionPoint == lowerCaseMarksWithDistinctPositions.size && count > 0)) {
+        // Moving left if before first mark, or moving right after last mark.
+        return null
+      }
+      if (count < 0) insertionPoint + count else insertionPoint + count - 1
+    }
+    // Excessive values of count.absoluteValue cause us to stop at the first/last mark.
+    val actualIndex = targetIndex.coerceIn(0, lowerCaseMarksWithDistinctPositions.lastIndex)
+    return lowerCaseMarksWithDistinctPositions[actualIndex]
+  }
+
+  /**
+   * Sorts [this] using [comparator], then drops elements that are duplicate by [comparator].
+   *
+   * This is more efficient than calling `sortedWith(comparator).distinctBy {}` because the distinct step can
+   * assume the input is sorted by the same Comparator.
+   */
+  private fun List<Mark>.sortedWithAndDistinctBy(comparator: Comparator<Mark>): List<Mark> {
+    val sorted = this.sortedWith(Mark.PositionSorter)
+    return sorted.fold(ArrayList<Mark>(sorted.size)) { outputList, mark ->
+        val previousMark = outputList.lastOrNull()
+        if (previousMark == null || Mark.PositionSorter.compare(previousMark, mark) != 0) {
+          outputList.add(mark)
+        }
+        outputList
+      }
+  }
+
   override fun getAllLocalMarks(caret: ImmutableVimCaret): Set<Mark> {
     val path = caret.editor.getPath() ?: return emptySet()
     val marks = if (caret.isPrimary) {
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroup.kt
index 89f3b4280..954b8265a 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroup.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroup.kt
@@ -83,6 +83,7 @@ interface VimMotionGroup {
 
   // Move caret to other
   fun moveCaretToMark(caret: ImmutableVimCaret, ch: Char, toLineStart: Boolean): Motion
+  fun moveCaretToMarkRelative(caret: ImmutableVimCaret, count: Int): Motion
   fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion
   fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion
 
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt
index 7f77fd4a4..5e252d2d9 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt
@@ -307,6 +307,13 @@ abstract class VimMotionGroupBase : VimMotionGroup {
     return Motion.Error
   }
 
+  override fun moveCaretToMarkRelative(caret: ImmutableVimCaret, count: Int): Motion {
+    val markService = injector.markService
+    val mark = markService.getRelativeLowercaseMark(caret, count) ?: return Motion.Error
+    val offset = caret.editor.bufferPositionToOffset(BufferPosition(mark.line, mark.col, false))
+    return offset.toMotionOrError()
+  }
+
   override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
     val jumpService = injector.jumpService
     val spot = jumpService.getJumpSpot(editor)
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/mark/Marks.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/mark/Marks.kt
index 3b98148c3..3e097433d 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/mark/Marks.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/mark/Marks.kt
@@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.mark
 
 import com.maddyhome.idea.vim.api.BufferPosition
 import com.maddyhome.idea.vim.api.VimEditor
+import com.maddyhome.idea.vim.mark.Mark.KeySorter.ORDER
 import org.jetbrains.annotations.NonNls
 
 interface Mark {
@@ -29,6 +30,13 @@ interface Mark {
       return ORDER.indexOf(o1.key) - ORDER.indexOf(o2.key)
     }
   }
+  // Same as in BufferPosition.
+  // TODO: Consider having a shared Interface / comparator for Mark and BufferPosition to avoid this duplication.
+  object PositionSorter: Comparator<Mark> {
+    override fun compare(o1: Mark, o2: Mark): Int {
+      return if (o1.line != o2.line) o1.line - o2.line else o1.col - o2.col
+    }
+  }
 }
 
 data class VimMark(
diff --git a/vim-engine/src/main/resources/ksp-generated/engine_commands.json b/vim-engine/src/main/resources/ksp-generated/engine_commands.json
index e312d52b1..bd9038198 100644
--- a/vim-engine/src/main/resources/ksp-generated/engine_commands.json
+++ b/vim-engine/src/main/resources/ksp-generated/engine_commands.json
@@ -1239,6 +1239,11 @@
         "class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionBackwardEndAction",
         "modes": "NXO"
     },
+    {
+        "keys": "[`",
+        "class": "com.maddyhome.idea.vim.action.motion.mark.MotionGotoPreviousMarkAction",
+        "modes": "NXO"
+    },
     {
         "keys": "[b",
         "class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelLeftAction",
@@ -1304,6 +1309,11 @@
         "class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionForwardStartAction",
         "modes": "NXO"
     },
+    {
+        "keys": "]`",
+        "class": "com.maddyhome.idea.vim.action.motion.mark.MotionGotoNextMarkAction",
+        "modes": "NXO"
+    },
     {
         "keys": "]b",
         "class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelEndLeftAction",