diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
new file mode 100644
index 0000000000000000000000000000000000000000..e16ddbfef7e99f731f5af4e2c86758cd72f77fd1
--- /dev/null
+++ b/app/assets/javascripts/manual_ordering.js
@@ -0,0 +1,58 @@
+import Sortable from 'sortablejs';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import {
+  getBoardSortableDefaultOptions,
+  sortableStart,
+} from '~/boards/mixins/sortable_default_options';
+import axios from '~/lib/utils/axios_utils';
+
+const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
+  axios
+    .put(`${url}/reorder`, {
+      move_before_id,
+      move_after_id,
+      group_full_path: issueList.dataset.groupFullPath,
+    })
+    .catch(() => {
+      createFlash(s__("ManualOrdering|Couldn't save the order of the issues"));
+    });
+
+const initManualOrdering = () => {
+  const issueList = document.querySelector('.manual-ordering');
+
+  if (!issueList || !(gon.features && gon.features.manualSorting)) {
+    return;
+  }
+
+  Sortable.create(
+    issueList,
+    getBoardSortableDefaultOptions({
+      scroll: true,
+      dataIdAttr: 'data-id',
+      fallbackOnBody: false,
+      group: {
+        name: 'issues',
+      },
+      draggable: 'li.issue',
+      onStart: () => {
+        sortableStart();
+      },
+      onUpdate: event => {
+        const el = event.item;
+
+        const url = el.getAttribute('url');
+
+        const prev = el.previousElementSibling;
+        const next = el.nextElementSibling;
+
+        const beforeId = prev && parseInt(prev.dataset.id, 10);
+        const afterId = next && parseInt(next.dataset.id, 10);
+
+        updateIssue(url, issueList, { move_after_id: afterId, move_before_id: beforeId });
+      },
+    }),
+  );
+};
+
+export default initManualOrdering;
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index 9055738f86e89f43069be4968068e6f3aa739b04..2ffeed8a58410c140c93d3396ef083a7ac38e87e 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
 import initFilteredSearch from '~/pages/search/init_filtered_search';
 import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
 import { FILTERED_SEARCH } from '~/pages/constants';
+import initManualOrdering from '~/manual_ordering';
 
 document.addEventListener('DOMContentLoaded', () => {
   initFilteredSearch({
@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
   });
 
   projectSelect();
+  initManualOrdering();
 });
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 35d4b0346546d0350582725a859afd3fe2d668ec..23fb5656008f695b261fbe5127c722ff3a9edd35 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
 import initFilteredSearch from '~/pages/search/init_filtered_search';
 import { FILTERED_SEARCH } from '~/pages/constants';
 import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import initManualOrdering from '~/manual_ordering';
 
 document.addEventListener('DOMContentLoaded', () => {
   IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -12,4 +13,5 @@ document.addEventListener('DOMContentLoaded', () => {
     filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
   });
   projectSelect();
+  initManualOrdering();
 });
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index c34aff02111b6a04791f0bdff7b21e61ee93269f..c73ebb31eb32ae53b0076bd72919e3b0accd5dce 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -7,6 +7,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
 import { FILTERED_SEARCH } from '~/pages/constants';
 import { ISSUABLE_INDEX } from '~/pages/projects/constants';
 import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import initManualOrdering from '~/manual_ordering';
 
 document.addEventListener('DOMContentLoaded', () => {
   IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
 
   new ShortcutsNavigation();
   new UsersSelect();
+  initManualOrdering();
 });
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 48289c8f381d133a906c054ee12d245bbea40f16..8359a60ec9f2fb6e5c7a083ad145f859ee870319 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,4 +1,18 @@
 .issues-list {
+  &.manual-ordering {
+    background-color: $gray-light;
+    border-radius: $border-radius-default;
+    padding: $gl-padding-8;
+
+    .issue {
+      background-color: $white-light;
+      margin-bottom: $gl-padding-8;
+      border-radius: $border-radius-default;
+      border: 1px solid $gray-100;
+      box-shadow: 0 1px 2px $issue-boards-card-shadow;
+    }
+  }
+
   .issue {
     padding: 10px 0 10px $gl-padding;
     position: relative;
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 63d5bd18bbe3d4df94473087299cbcdde44e05e1..73921460b8c9a10ac3c3a2e4c13a033f33f9cd40 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -7,6 +7,10 @@ class GroupsController < Groups::ApplicationController
   include PreviewMarkdown
   include RecordUserLastActivity
 
+  before_action do
+    push_frontend_feature_flag(:manual_sorting)
+  end
+
   respond_to :html
 
   prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index bc7dc210d191018dab62196de6bc97f47fa113ca..32d1ed066ea29780229e5cced3f399de1410b6eb 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,6 +10,10 @@ class Projects::IssuesController < Projects::ApplicationController
   include SpammableActions
   include RecordUserLastActivity
 
+  before_action do
+    push_frontend_feature_flag(:manual_sorting)
+  end
+
   def issue_except_actions
     %i[index calendar new create bulk_update import_csv]
   end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 9f3115afe8967b9edacef2651f74806374101fdf..387cdf995c75ec23bdbcdb939a5ea88372870c76 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -5,6 +5,7 @@ module IssuesHelper
     classes = ["issue"]
     classes << "closed" if issue.closed?
     classes << "today" if issue.today?
+    classes << "user-can-drag" if @sort == 'relative_position'
     classes.join(' ')
   end
 
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 6f7713124acbb4fe919f46021ee965c2d9250afa..7d539c9d7494fed0b7814ef836dabdfda8f1236e 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,6 +1,6 @@
 - empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
 
-%ul.content-list.issues-list.issuable-list
+%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
   = render partial: "projects/issues/issue", collection: @issues
   - if @issues.blank?
     = render empty_state_path
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 987a5d4f13f705f985c938d39ba3bc53f5cfc746..a21dcabb485f4d51949070707d4fa00ea5629af1 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,6 +1,6 @@
 - if @issues.to_a.any?
   .card.card-small.card-without-border
-    %ul.content-list.issues-list.issuable-list
+    %ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
       = render partial: 'projects/issues/issue', collection: @issues
   = paginate @issues, theme: "gitlab"
 - else
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index 1dd97bc4ed1142fae684861799442706e82b8716..403e001bfe8b056a072b3e3cf7e8c130ad59acda 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -1,6 +1,7 @@
 - sort_value = @sort
 - sort_title = issuable_sort_option_title(sort_value)
 - viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
+- manual_sorting = viewing_issues && controller.controller_name != 'dashboard' && Feature.enabled?(:manual_sorting)
 
 .dropdown.inline.prepend-left-10.issue-sort-dropdown
   .btn-group{ role: 'group' }
@@ -17,6 +18,6 @@
           = sortable_item(sort_title_due_date,          page_filter_path(sort: sort_value_due_date),          sort_title) if viewing_issues
           = sortable_item(sort_title_popularity,        page_filter_path(sort: sort_value_popularity),        sort_title)
           = sortable_item(sort_title_label_priority,    page_filter_path(sort: sort_value_label_priority),    sort_title)
-          = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues && Feature.enabled?(:manual_sorting)
+          = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if manual_sorting
           = render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
     = issuable_sort_direction_button(sort_value)
diff --git a/changelogs/unreleased/fe-issue-reorder.yml b/changelogs/unreleased/fe-issue-reorder.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aca334b6149b4f767f4a823238b701e4136bca2f
--- /dev/null
+++ b/changelogs/unreleased/fe-issue-reorder.yml
@@ -0,0 +1,5 @@
+---
+title: Bring Manual Ordering on Issue List
+merge_request: 29410
+author:
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3a0288e416928a947e3b0cf404d032d3f8e935f7..024cd98ca0f98514aa7dfaef45cda57c6b168660 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8038,6 +8038,9 @@ msgstr ""
 msgid "Manual job"
 msgstr ""
 
+msgid "ManualOrdering|Couldn't save the order of the issues"
+msgstr ""
+
 msgid "Map a FogBugz account ID to a GitLab user"
 msgstr ""
 
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 176f4a668ffc6f0f64b39a95215f23b0900baedd..2e7525e0513d1465d66c913c26445be25407a658 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
 
 describe 'Group issues page' do
   include FilteredSearchHelpers
+  include DragTo
 
   let(:group) { create(:group) }
   let(:project) { create(:project, :public, group: group)}
@@ -99,4 +100,49 @@ describe 'Group issues page' do
       end
     end
   end
+
+  context 'manual ordering' do
+    let!(:issue1) { create(:issue, project: project, title: 'Issue #1') }
+    let!(:issue2) { create(:issue, project: project, title: 'Issue #2') }
+    let!(:issue3) { create(:issue, project: project, title: 'Issue #3') }
+
+    it 'displays all issues' do
+      visit issues_group_path(group, sort: 'relative_position')
+
+      page.within('.issues-list') do
+        expect(page).to have_selector('li.issue', count: 3)
+      end
+    end
+
+    it 'has manual-ordering css applied' do
+      visit issues_group_path(group, sort: 'relative_position')
+
+      expect(page).to have_selector('.manual-ordering')
+    end
+
+    it 'each issue item has a user-can-drag css applied' do
+      visit issues_group_path(group, sort: 'relative_position')
+
+      page.within('.manual-ordering') do
+        expect(page).to have_selector('.issue.user-can-drag', count: 3)
+      end
+    end
+
+    it 'issues should be draggable and persist order', :js do
+      visit issues_group_path(group, sort: 'relative_position')
+
+      drag_to(selector: '.manual-ordering',
+        scrollable: '#board-app',
+        list_from_index: 0,
+        from_index: 0,
+        to_index: 2,
+        list_to_index: 0)
+
+      page.within('.manual-ordering') do
+        expect(find('.issue:nth-child(1) .title')).to have_content('Issue #2')
+        expect(find('.issue:nth-child(2) .title')).to have_content('Issue #1')
+        expect(find('.issue:nth-child(3) .title')).to have_content('Issue #3')
+      end
+    end
+  end
 end