diff --git a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
index 4755977b05174e55a01835dabefe6201dbc06ff9..426d377c92b489d2a2f6753c0aa5bc5bb56ce90f 100644
--- a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
+++ b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
@@ -1,8 +1,10 @@
 <script>
 import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
+import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
 import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
 import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
 
 export default {
   components: {
@@ -10,6 +12,7 @@ export default {
     GlSprintf,
     ClipboardButton,
     RunnerInstructions,
+    RunnerRegistrationTokenReset,
   },
   directives: {
     GlTooltip: GlTooltipDirective,
@@ -24,16 +27,40 @@ export default {
       type: String,
       required: true,
     },
-    typeName: {
+    type: {
       type: String,
-      required: false,
-      default: __('shared'),
+      required: true,
+      validator(type) {
+        return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+      },
     },
   },
+  data() {
+    return {
+      currentRegistrationToken: this.registrationToken,
+    };
+  },
   computed: {
     rootUrl() {
       return gon.gitlab_url || '';
     },
+    typeName() {
+      switch (this.type) {
+        case INSTANCE_TYPE:
+          return s__('Runners|shared');
+        case GROUP_TYPE:
+          return s__('Runners|group');
+        case PROJECT_TYPE:
+          return s__('Runners|specific');
+        default:
+          return '';
+      }
+    },
+  },
+  methods: {
+    onTokenReset(token) {
+      this.currentRegistrationToken = token;
+    },
   },
 };
 </script>
@@ -65,12 +92,13 @@ export default {
         {{ __('And this registration token:') }}
         <br />
 
-        <code data-testid="registration-token">{{ registrationToken }}</code>
-        <clipboard-button :title="__('Copy token')" :text="registrationToken" />
+        <code data-testid="registration-token">{{ currentRegistrationToken }}</code>
+        <clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" />
       </li>
     </ol>
 
-    <!-- TODO Implement reset token functionality -->
+    <runner-registration-token-reset :type="type" @tokenReset="onTokenReset" />
+
     <runner-instructions />
   </div>
 </template>
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b03574264d98641c38a3d820b381cff9e0e94fd0
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import { __, s__ } from '~/locale';
+import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
+
+export default {
+  components: {
+    GlButton,
+  },
+  props: {
+    type: {
+      type: String,
+      required: true,
+      validator(type) {
+        return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+      },
+    },
+  },
+  data() {
+    return {
+      loading: false,
+    };
+  },
+  computed: {},
+  methods: {
+    async resetToken() {
+      // TODO Replace confirmation with gl-modal
+      // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
+      // eslint-disable-next-line no-alert
+      if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
+        return;
+      }
+
+      this.loading = true;
+      try {
+        const {
+          data: {
+            runnersRegistrationTokenReset: { token, errors },
+          },
+        } = await this.$apollo.mutate({
+          mutation: runnersRegistrationTokenResetMutation,
+          variables: {
+            // TODO Currently INTANCE_TYPE only is supported
+            // In future iterations this component will support
+            // other registration token types.
+            // See: https://gitlab.com/gitlab-org/gitlab/-/issues/19819
+            input: {
+              type: this.type,
+            },
+          },
+        });
+        if (errors && errors.length) {
+          this.onError(new Error(errors[0]));
+          return;
+        }
+        this.onSuccess(token);
+      } catch (e) {
+        this.onError(e);
+      } finally {
+        this.loading = false;
+      }
+    },
+    onError(error) {
+      const { message } = error;
+      createFlash({ message });
+    },
+    onSuccess(token) {
+      createFlash({
+        message: s__('Runners|New registration token generated!'),
+        type: FLASH_TYPES.SUCCESS,
+      });
+      this.$emit('tokenReset', token);
+    },
+  },
+};
+</script>
+<template>
+  <gl-button :loading="loading" @click="resetToken">
+    {{ __('Reset registration token') }}
+  </gl-button>
+</template>
diff --git a/app/assets/javascripts/runner/graphql/runners_registration_token_reset.mutation.graphql b/app/assets/javascripts/runner/graphql/runners_registration_token_reset.mutation.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..9c2797732ad56b0769feb889a5f9bacd3108e35b
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/runners_registration_token_reset.mutation.graphql
@@ -0,0 +1,6 @@
+mutation runnersRegistrationTokenReset($input: RunnersRegistrationTokenResetInput!) {
+  runnersRegistrationTokenReset(input: $input) {
+    token
+    errors
+  }
+}
diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
index b4eacb911a2bf6205ee1316456a42ae690babc67..7f3a980cccaa29d279182793e707a444288dd6c1 100644
--- a/app/assets/javascripts/runner/runner_list/runner_list_app.vue
+++ b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
@@ -7,6 +7,7 @@ import RunnerList from '../components/runner_list.vue';
 import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
 import RunnerPagination from '../components/runner_pagination.vue';
 import RunnerTypeHelp from '../components/runner_type_help.vue';
+import { INSTANCE_TYPE } from '../constants';
 import getRunnersQuery from '../graphql/get_runners.query.graphql';
 import {
   fromUrlQueryToSearch,
@@ -97,6 +98,7 @@ export default {
       });
     },
   },
+  INSTANCE_TYPE,
 };
 </script>
 <template>
@@ -106,7 +108,10 @@ export default {
         <runner-type-help />
       </div>
       <div class="col-sm-6">
-        <runner-manual-setup-help :registration-token="registrationToken" />
+        <runner-manual-setup-help
+          :registration-token="registrationToken"
+          :type="$options.INSTANCE_TYPE"
+        />
       </div>
     </div>
 
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ff7dc0f58c6fa285c15c3150c6eb2c6949ebfe62..1d63a5fb16abfb973f37f7cdfd6cc35d8974a142 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -28273,6 +28273,9 @@ msgstr ""
 msgid "Runners|Name"
 msgstr ""
 
+msgid "Runners|New registration token generated!"
+msgstr ""
+
 msgid "Runners|New runner, has not connected yet"
 msgstr ""
 
diff --git a/spec/frontend/runner/components/runner_manual_setup_help_spec.js b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
index ca5c88f6e28036210496c6f45264e61820bae996..add595d784e5022f7a507c0c2bde921814b15693 100644
--- a/spec/frontend/runner/components/runner_manual_setup_help_spec.js
+++ b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
@@ -1,8 +1,11 @@
 import { GlSprintf } from '@gitlab/ui';
 import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
 import { TEST_HOST } from 'helpers/test_constants';
 import { extendedWrapper } from 'helpers/vue_test_utils_helper';
 import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
 import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
 import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
 
@@ -14,6 +17,8 @@ describe('RunnerManualSetupHelp', () => {
   let originalGon;
 
   const findRunnerInstructions = () => wrapper.findComponent(RunnerInstructions);
+  const findRunnerRegistrationTokenReset = () =>
+    wrapper.findComponent(RunnerRegistrationTokenReset);
   const findClipboardButtons = () => wrapper.findAllComponents(ClipboardButton);
   const findRunnerHelpTitle = () => wrapper.findByTestId('runner-help-title');
   const findCoordinatorUrl = () => wrapper.findByTestId('coordinator-url');
@@ -28,6 +33,7 @@ describe('RunnerManualSetupHelp', () => {
         },
         propsData: {
           registrationToken: mockRegistrationToken,
+          type: INSTANCE_TYPE,
           ...props,
         },
         stubs: {
@@ -54,16 +60,26 @@ describe('RunnerManualSetupHelp', () => {
     wrapper.destroy();
   });
 
-  it('Title contains the default runner type', () => {
+  it('Title contains the shared runner type', () => {
+    createComponent({ props: { type: INSTANCE_TYPE } });
+
     expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a shared runner manually');
   });
 
   it('Title contains the group runner type', () => {
-    createComponent({ props: { typeName: 'group' } });
+    createComponent({ props: { type: GROUP_TYPE } });
 
     expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a group runner manually');
   });
 
+  it('Title contains the specific runner type', () => {
+    createComponent({ props: { type: PROJECT_TYPE } });
+
+    expect(findRunnerHelpTitle().text()).toMatchInterpolatedText(
+      'Set up a specific runner manually',
+    );
+  });
+
   it('Runner Install Page link', () => {
     expect(findRunnerHelpLink().attributes('href')).toBe(mockRunnerInstallHelpPage);
   });
@@ -73,12 +89,27 @@ describe('RunnerManualSetupHelp', () => {
     expect(findClipboardButtons().at(0).props('text')).toBe(TEST_HOST);
   });
 
+  it('Displays the runner instructions', () => {
+    expect(findRunnerInstructions().exists()).toBe(true);
+  });
+
   it('Displays the registration token', () => {
     expect(findRegistrationToken().text()).toBe(mockRegistrationToken);
     expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken);
   });
 
-  it('Displays the runner instructions', () => {
-    expect(findRunnerInstructions().exists()).toBe(true);
+  it('Displays the runner registration token reset button', () => {
+    expect(findRunnerRegistrationTokenReset().exists()).toBe(true);
+  });
+
+  it('Replaces the runner reset button', async () => {
+    const mockNewRegistrationToken = 'NEW_MOCK_REGISTRATION_TOKEN';
+
+    findRunnerRegistrationTokenReset().vm.$emit('tokenReset', mockNewRegistrationToken);
+
+    await nextTick();
+
+    expect(findRegistrationToken().text()).toBe(mockNewRegistrationToken);
+    expect(findClipboardButtons().at(1).props('text')).toBe(mockNewRegistrationToken);
   });
 });
diff --git a/spec/frontend/runner/components/runner_registration_token_reset_spec.js b/spec/frontend/runner/components/runner_registration_token_reset_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa5751b380fee577065747438939c23dd0e92ec4
--- /dev/null
+++ b/spec/frontend/runner/components/runner_registration_token_reset_spec.js
@@ -0,0 +1,155 @@
+import { GlButton } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
+import { INSTANCE_TYPE } from '~/runner/constants';
+import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const mockNewToken = 'NEW_TOKEN';
+
+describe('RunnerRegistrationTokenReset', () => {
+  let wrapper;
+  let runnersRegistrationTokenResetMutationHandler;
+
+  const findButton = () => wrapper.findComponent(GlButton);
+
+  const createComponent = () => {
+    wrapper = shallowMount(RunnerRegistrationTokenReset, {
+      localVue,
+      propsData: {
+        type: INSTANCE_TYPE,
+      },
+      apolloProvider: createMockApollo([
+        [runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
+      ]),
+    });
+  };
+
+  beforeEach(() => {
+    runnersRegistrationTokenResetMutationHandler = jest.fn().mockResolvedValue({
+      data: {
+        runnersRegistrationTokenReset: {
+          token: mockNewToken,
+          errors: [],
+        },
+      },
+    });
+
+    createComponent();
+
+    jest.spyOn(window, 'confirm');
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  it('Displays reset button', () => {
+    expect(findButton().exists()).toBe(true);
+  });
+
+  describe('On click and confirmation', () => {
+    beforeEach(async () => {
+      window.confirm.mockReturnValueOnce(true);
+      await findButton().vm.$emit('click');
+    });
+
+    it('resets token', () => {
+      expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledTimes(1);
+      expect(runnersRegistrationTokenResetMutationHandler).toHaveBeenCalledWith({
+        input: { type: INSTANCE_TYPE },
+      });
+    });
+
+    it('emits result', () => {
+      expect(wrapper.emitted('tokenReset')).toHaveLength(1);
+      expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]);
+    });
+
+    it('does not show a loading state', () => {
+      expect(findButton().props('loading')).toBe(false);
+    });
+
+    it('shows confirmation', () => {
+      expect(createFlash).toHaveBeenLastCalledWith({
+        message: expect.stringContaining('registration token generated'),
+        type: FLASH_TYPES.SUCCESS,
+      });
+    });
+  });
+
+  describe('On click without confirmation', () => {
+    beforeEach(async () => {
+      window.confirm.mockReturnValueOnce(false);
+      await findButton().vm.$emit('click');
+    });
+
+    it('does not reset token', () => {
+      expect(runnersRegistrationTokenResetMutationHandler).not.toHaveBeenCalled();
+    });
+
+    it('does not emit any result', () => {
+      expect(wrapper.emitted('tokenReset')).toBeUndefined();
+    });
+
+    it('does not show a loading state', () => {
+      expect(findButton().props('loading')).toBe(false);
+    });
+
+    it('does not shows confirmation', () => {
+      expect(createFlash).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('On error', () => {
+    it('On network error, error message is shown', async () => {
+      runnersRegistrationTokenResetMutationHandler.mockRejectedValueOnce(
+        new Error('Something went wrong'),
+      );
+
+      window.confirm.mockReturnValueOnce(true);
+      await findButton().vm.$emit('click');
+      await waitForPromises();
+
+      expect(createFlash).toHaveBeenLastCalledWith({
+        message: 'Network error: Something went wrong',
+      });
+    });
+
+    it('On validation error, error message is shown', async () => {
+      runnersRegistrationTokenResetMutationHandler.mockResolvedValue({
+        data: {
+          runnersRegistrationTokenReset: {
+            token: null,
+            errors: ['Token reset failed'],
+          },
+        },
+      });
+
+      window.confirm.mockReturnValueOnce(true);
+      await findButton().vm.$emit('click');
+      await waitForPromises();
+
+      expect(createFlash).toHaveBeenLastCalledWith({
+        message: 'Token reset failed',
+      });
+    });
+  });
+
+  describe('Immediately after click', () => {
+    it('shows loading state', async () => {
+      window.confirm.mockReturnValue(true);
+      await findButton().vm.$emit('click');
+
+      expect(findButton().props('loading')).toBe(true);
+    });
+  });
+});