diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 8434ef5cc7a1d942df0c58c55258f38e9b6954c2..6695d9fe96cea5a6161877ae46032f2571c625e6 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -109,7 +109,7 @@ export const toggleLineDiscussions = ({ commit }, options) => {
 export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
   const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
 
-  if (discussion) {
+  if (discussion && discussion.diff_file) {
     const file = state.diffFiles.find(f => f.file_hash === discussion.diff_file.file_hash);
 
     if (file) {
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 842a209a54521072d38544d5c7e18b1705ae0aac..8caac68e0d4883f01dde775f11f4a806ccd4d8ab 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
 import { mapActions, mapState, mapGetters } from 'vuex';
 import store from 'ee_else_ce/mr_notes/stores';
 import notesApp from '../notes/components/notes_app.vue';
+import discussionKeyboardNavigator from '../notes/components/discussion_keyboard_navigator.vue';
 
 export default () => {
   // eslint-disable-next-line no-new
@@ -56,15 +57,19 @@ export default () => {
       },
     },
     render(createElement) {
-      return createElement('notes-app', {
-        props: {
-          noteableData: this.noteableData,
-          notesData: this.notesData,
-          userData: this.currentUserData,
-          shouldShow: this.activeTab === 'show',
-          helpPagePath: this.helpPagePath,
-        },
-      });
+      const isDiffView = this.activeTab === 'diffs';
+
+      return createElement(discussionKeyboardNavigator, { props: { isDiffView } }, [
+        createElement('notes-app', {
+          props: {
+            noteableData: this.noteableData,
+            notesData: this.notesData,
+            userData: this.currentUserData,
+            shouldShow: this.activeTab === 'show',
+            helpPagePath: this.helpPagePath,
+          },
+        }),
+      ]);
     },
   });
 };
diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5fc2b6ba04c0ebf37d895fa521900a4667a64766
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
@@ -0,0 +1,47 @@
+<script>
+/* global Mousetrap */
+import 'mousetrap';
+import { mapGetters, mapActions } from 'vuex';
+import discussionNavigation from '~/notes/mixins/discussion_navigation';
+
+export default {
+  mixins: [discussionNavigation],
+  props: {
+    isDiffView: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      currentDiscussionId: null,
+    };
+  },
+  computed: {
+    ...mapGetters(['nextUnresolvedDiscussionId', 'previousUnresolvedDiscussionId']),
+  },
+  mounted() {
+    Mousetrap.bind('n', () => this.jumpToNextDiscussion());
+    Mousetrap.bind('p', () => this.jumpToPreviousDiscussion());
+  },
+  methods: {
+    ...mapActions(['expandDiscussion']),
+    jumpToNextDiscussion() {
+      const nextId = this.nextUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
+
+      this.jumpToDiscussion(nextId);
+      this.currentDiscussionId = nextId;
+    },
+    jumpToPreviousDiscussion() {
+      const prevId = this.previousUnresolvedDiscussionId(this.currentDiscussionId, this.isDiffView);
+
+      this.jumpToDiscussion(prevId);
+      this.currentDiscussionId = prevId;
+    },
+  },
+  render() {
+    return this.$slots.default;
+  },
+};
+</script>
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 8aa8f5037b3fd1917a27e0751619294c2113ee79..52410f18d4aa278e46ef3dfc1827642a82b3d586 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -183,6 +183,15 @@ export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, dif
   return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0];
 };
 
+export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
+  const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+  const currentIndex = idsOrdered.indexOf(discussionId);
+  const slicedIds = idsOrdered.slice(currentIndex - 1, currentIndex);
+
+  // Get the last ID if there is none after the currentIndex
+  return slicedIds.length ? slicedIds[0] : idsOrdered[idsOrdered.length - 1];
+};
+
 // @param {Boolean} diffOrder - is ordered by diff?
 export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
   if (diffOrder) {
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 46d7c367aa7acb22f6e664f462965687ab42d97e..0624f66b3a52678a83d946ececcf43bb61ffd41d 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -332,7 +332,7 @@
                   %td.shortcut
                     %kbd l
                   %td Change Label
-              %tbody.hidden-shortcut{ style: 'display:none' }
+              %tbody.hidden-shortcut.merge_requests{ style: 'display:none' }
                 %tr
                   %th
                   %th Merge Requests
@@ -368,6 +368,14 @@
                     \/
                     %kbd k
                   %td Move to previous file
+                %tr
+                  %td.shortcut
+                    %kbd n
+                  %td= _("Move to next unresolved discussion")
+                %tr
+                  %td.shortcut
+                    %kbd p
+                  %td= _("Move to previous unresolved discussion")
               %tbody.hidden-shortcut{ style: 'display:none' }
                 %tr
                   %th
diff --git a/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml b/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml
new file mode 100644
index 0000000000000000000000000000000000000000..02e81c7fc8785f33246ec27bb51a313f5d782e9a
--- /dev/null
+++ b/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Keyboard shortcut for jump to NEXT unresolved discussion
+merge_request: 30144
+author:
+type: added
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 3cb765c0463e87af8c94a72003e14030b41af345..f844f56557fee5e57d5d0b147def0ba68e68f1a7 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -88,6 +88,11 @@ Jump button next to the Reply field on a thread.
 You can also jump to the first unresolved thread from the button next to the
 resolved threads tracker.
 
+You can also use keyboard shortcuts to navigate among threads:
+
+- Use <kbd>n</kbd> to jump to the next unresolved thread.
+- Use <kbd>p</kbd> to jump to the previous unresolved thread.
+
 !["8/9 threads resolved"](img/threads_resolved.png)
 
 ### Marking a comment or thread as resolved
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index d61d7eafd182c834faf063ecbc2966f3ce5bf707..5d08bf5e77d1c84df26c63ed8874c37b46fb555f 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -84,6 +84,8 @@ You can see GitLab's keyboard shortcuts by using <kbd>shift</kbd> + <kbd>?</kbd>
 | <kbd>l</kbd> | Change label |
 | <kbd>]</kbd> or <kbd>j</kbd> | Move to next file |
 | <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file |
+| <kbd>n</kbd> | Move to next unresolved discussion     |
+| <kbd>p</kbd> | Move to previous unresolved discussion |
 
 ## Epics **(ULTIMATE)**
 
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7dd6e0f2a937d555243973f964d2bc99f5dc8d0a..e8a6ab31814ec1315e6ab5784d47059995c2b6a3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6952,6 +6952,12 @@ msgstr ""
 msgid "Move this issue to another project."
 msgstr ""
 
+msgid "Move to next unresolved discussion"
+msgstr ""
+
+msgid "Move to previous unresolved discussion"
+msgstr ""
+
 msgid "MoveIssue|Cannot move issue due to insufficient permissions!"
 msgstr ""
 
diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6d50713999ddc05ada7ae57fe7d1adaee3b20e43
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
@@ -0,0 +1,77 @@
+/* global Mousetrap */
+import 'mousetrap';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import DiscussionKeyboardNavigator from '~/notes/components/discussion_keyboard_navigator.vue';
+import notesModule from '~/notes/stores/modules';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+const NEXT_ID = 'abc123';
+const PREV_ID = 'def456';
+const NEXT_DIFF_ID = 'abc123_diff';
+const PREV_DIFF_ID = 'def456_diff';
+
+describe('notes/components/discussion_keyboard_navigator', () => {
+  let storeOptions;
+  let wrapper;
+  let store;
+
+  const createComponent = (options = {}) => {
+    store = new Vuex.Store(storeOptions);
+
+    wrapper = shallowMount(DiscussionKeyboardNavigator, {
+      localVue,
+      store,
+      ...options,
+    });
+
+    wrapper.vm.jumpToDiscussion = jest.fn();
+  };
+
+  beforeEach(() => {
+    const notes = notesModule();
+
+    notes.getters.nextUnresolvedDiscussionId = () => (currId, isDiff) =>
+      isDiff ? NEXT_DIFF_ID : NEXT_ID;
+    notes.getters.previousUnresolvedDiscussionId = () => (currId, isDiff) =>
+      isDiff ? PREV_DIFF_ID : PREV_ID;
+
+    storeOptions = {
+      modules: {
+        notes,
+      },
+    };
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+    storeOptions = null;
+    store = null;
+  });
+
+  describe.each`
+    isDiffView | expectedNextId  | expectedPrevId
+    ${true}    | ${NEXT_DIFF_ID} | ${PREV_DIFF_ID}
+    ${false}   | ${NEXT_ID}      | ${PREV_ID}
+  `('when isDiffView is $isDiffView', ({ isDiffView, expectedNextId, expectedPrevId }) => {
+    beforeEach(() => {
+      createComponent({ propsData: { isDiffView } });
+    });
+
+    it('calls jumpToNextDiscussion when pressing `n`', () => {
+      Mousetrap.trigger('n');
+
+      expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedNextId);
+      expect(wrapper.vm.currentDiscussionId).toEqual(expectedNextId);
+    });
+
+    it('calls jumpToPreviousDiscussion when pressing `p`', () => {
+      Mousetrap.trigger('p');
+
+      expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(expectedPrevId);
+      expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId);
+    });
+  });
+});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index c3ed079e33bca9374086f19eed3fc0f16eb628fe..71dcba114a9cf591533b4564a702e0a16063f97b 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -256,6 +256,54 @@ describe('Getters Notes Store', () => {
     });
   });
 
+  describe('previousUnresolvedDiscussionId', () => {
+    describe('with unresolved discussions', () => {
+      const localGetters = {
+        unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
+      };
+
+      it('with bogus returns falsey', () => {
+        expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('456');
+      });
+
+      [
+        { id: '123', expected: '789' },
+        { id: '456', expected: '123' },
+        { id: '789', expected: '456' },
+      ].forEach(({ id, expected }) => {
+        it(`with ${id}, returns previous value`, () => {
+          expect(getters.previousUnresolvedDiscussionId(state, localGetters)(id)).toBe(expected);
+        });
+      });
+    });
+
+    describe('with 1 unresolved discussion', () => {
+      const localGetters = {
+        unresolvedDiscussionsIdsOrdered: () => ['123'],
+      };
+
+      it('with bogus returns id', () => {
+        expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('123');
+      });
+
+      it('with match, returns value', () => {
+        expect(getters.previousUnresolvedDiscussionId(state, localGetters)('123')).toEqual('123');
+      });
+    });
+
+    describe('with 0 unresolved discussions', () => {
+      const localGetters = {
+        unresolvedDiscussionsIdsOrdered: () => [],
+      };
+
+      it('returns undefined', () => {
+        expect(
+          getters.previousUnresolvedDiscussionId(state, localGetters)('bogus'),
+        ).toBeUndefined();
+      });
+    });
+  });
+
   describe('firstUnresolvedDiscussionId', () => {
     const localGetters = {
       unresolvedDiscussionsIdsByDate: ['123', '456'],