diff --git a/docs/rules/index.md b/docs/rules/index.md
index 1ba5978d2..4085ce237 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -270,6 +270,7 @@ For example:
 | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
 | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
 | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
+| [vue/prefer-use-template-ref](./prefer-use-template-ref.md) | require using `useTemplateRef` instead of `ref` for template refs |  | :hammer: |
 | [vue/require-default-export](./require-default-export.md) | require components to be the default export |  | :warning: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  | :hammer: |
 | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |
diff --git a/docs/rules/prefer-use-template-ref.md b/docs/rules/prefer-use-template-ref.md
new file mode 100644
index 000000000..6d7fde89a
--- /dev/null
+++ b/docs/rules/prefer-use-template-ref.md
@@ -0,0 +1,71 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/prefer-use-template-ref
+description: require using `useTemplateRef` instead of `ref` for template refs
+---
+
+# vue/prefer-use-template-ref
+
+> require using `useTemplateRef` instead of `ref` for template refs
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
+
+## :book: Rule Details
+
+Vue 3.5 introduced a new way of obtaining template refs via
+the [`useTemplateRef()`](https://vuejs.org/guide/essentials/template-refs.html#accessing-the-refs) API.
+
+This rule enforces using the new `useTemplateRef` function instead of `ref` for template refs.
+
+<eslint-code-block :rules="{'vue/prefer-use-template-ref': ['error']}">
+
+```vue
+<template>
+  <button ref="submitRef">Submit</button>
+  <button ref="closeRef">Close</button>
+</template>
+
+<script setup>
+  import { ref, useTemplateRef } from 'vue';
+
+  /* ✓ GOOD */
+  const submitRef = useTemplateRef('submitRef');
+  const submitButton = useTemplateRef('submitRef');
+  const closeButton = useTemplateRef('closeRef');
+
+  /* ✗ BAD */
+  const closeRef = ref();
+</script>
+```
+
+</eslint-code-block>
+
+This rule skips `ref` template function refs as these should be used to allow custom implementation of storing `ref`. If you prefer
+`useTemplateRef`, you have to change the value of the template `ref` to a string.
+
+<eslint-code-block :rules="{'vue/prefer-use-template-ref': ['error']}">
+
+```vue
+<template>
+  <button :ref="ref => button = ref">Content</button>
+</template>
+
+<script setup>
+  import { ref } from 'vue';
+  
+  /* ✓ GOOD */
+  const button = ref();
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-use-template-ref.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-use-template-ref.js)
diff --git a/lib/index.js b/lib/index.js
index bb4abf40f..b09e247d5 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -208,6 +208,7 @@ const plugin = {
     'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
     'prefer-template': require('./rules/prefer-template'),
     'prefer-true-attribute-shorthand': require('./rules/prefer-true-attribute-shorthand'),
+    'prefer-use-template-ref': require('./rules/prefer-use-template-ref'),
     'prop-name-casing': require('./rules/prop-name-casing'),
     'quote-props': require('./rules/quote-props'),
     'require-component-is': require('./rules/require-component-is'),
diff --git a/lib/rules/prefer-use-template-ref.js b/lib/rules/prefer-use-template-ref.js
new file mode 100644
index 000000000..8dcdccb38
--- /dev/null
+++ b/lib/rules/prefer-use-template-ref.js
@@ -0,0 +1,89 @@
+/**
+ * @author Thomasan1999
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+/** @param expression {Expression | null} */
+function expressionIsRef(expression) {
+  // @ts-ignore
+  return expression?.callee?.name === 'ref'
+}
+
+/** @type {import("eslint").Rule.RuleModule} */
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'require using `useTemplateRef` instead of `ref` for template refs',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/prefer-use-template-ref.html'
+    },
+    schema: [],
+    messages: {
+      preferUseTemplateRef: "Replace 'ref' with 'useTemplateRef'."
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type Set<string> */
+    const templateRefs = new Set()
+
+    /**
+     * @typedef ScriptRef
+     * @type {{node: Expression, ref: string}}
+     */
+
+    /**
+     * @type ScriptRef[] */
+    const scriptRefs = []
+
+    return utils.compositingVisitors(
+      utils.defineTemplateBodyVisitor(
+        context,
+        {
+          'VAttribute[directive=false]'(node) {
+            if (node.key.name === 'ref' && node.value?.value) {
+              templateRefs.add(node.value.value)
+            }
+          }
+        },
+        {
+          VariableDeclarator(declarator) {
+            if (!expressionIsRef(declarator.init)) {
+              return
+            }
+
+            scriptRefs.push({
+              // @ts-ignore
+              node: declarator.init,
+              // @ts-ignore
+              ref: declarator.id.name
+            })
+          }
+        }
+      ),
+      {
+        'Program:exit'() {
+          for (const templateRef of templateRefs) {
+            const scriptRef = scriptRefs.find(
+              (scriptRef) => scriptRef.ref === templateRef
+            )
+
+            if (!scriptRef) {
+              continue
+            }
+
+            context.report({
+              node: scriptRef.node,
+              messageId: 'preferUseTemplateRef'
+            })
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/tests/lib/rules/prefer-use-template-ref.js b/tests/lib/rules/prefer-use-template-ref.js
new file mode 100644
index 000000000..49a2f0759
--- /dev/null
+++ b/tests/lib/rules/prefer-use-template-ref.js
@@ -0,0 +1,323 @@
+/**
+ * @author Thomasan1999
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/prefer-use-template-ref')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('prefer-use-template-ref', rule, {
+  valid: [
+    {
+      filename: 'single-use-template-ref.vue',
+      code: `
+      <template>
+          <div ref="root" />
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        const root = useTemplateRef('root');
+      </script>
+      `
+    },
+    {
+      filename: 'multiple-use-template-refs.vue',
+      code: `
+      <template>
+          <button ref="button">Content</button>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        const buttonRef = useTemplateRef('button');
+        const link = useTemplateRef('link');
+      </script>
+      `
+    },
+    {
+      filename: 'use-template-ref-in-block.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li ref="firstListItem">Morning</li>
+              <li>Afternoon</li>
+              <li>Evening</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { useTemplateRef } from 'vue';
+        function getFirstListItemElement() {
+          const firstListItem = useTemplateRef('firstListItem')
+          console.log(firstListItem)
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'non-template-ref.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li v-for="food in foods" :key="food">{{food}}</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const foods = ref(['Spaghetti', 'Pizza', 'Cake']);
+      </script>
+      `
+    },
+    {
+      filename: 'counter.js',
+      code: `
+      import { ref } from 'vue';
+      const counter = ref(0);
+      const names = ref(new Set());
+      function incrementCounter() {
+        counter.value++;
+        return counter.value;
+      }
+      function storeName(name) {
+        names.value.add(name)
+      }
+      `
+    },
+    {
+      filename: 'setup-function.vue',
+      code: `
+      <template>
+        <p>Button clicked {{counter}} times.</p>
+        <button ref="button" @click="counter++">Click</button>
+      </template>
+      <script>
+        import { ref, useTemplateRef } from 'vue';
+        export default {
+          name: 'Counter',
+          setup() {
+            const counter = ref(0);
+            const button = useTemplateRef('button');
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'options-api-no-refs.vue',
+      code: `
+      <template>
+        <label ref="label">
+          Name:
+          <input v-model="name" />
+        </label>
+        <p ref="textRef">{{niceName}}</p>
+        <button
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'NameRow',
+          methods: {
+            someFunction() {
+              return {
+                label: ref(5),
+              }
+            }
+          }
+          data() {
+            return {
+              label: 'label',
+              name: ''
+            }
+          },
+          computed: {
+            niceName() {
+              return 'Nice ' + this.name;
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'options-api-mixed.vue',
+      code: `
+      <template>
+        <label ref="labelElem">
+          Name:
+          <input v-model="name" />
+        </label>
+        {{ loremIpsum }}
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'NameRow',
+          props: {
+            defaultLabel: {
+              type: String,
+            },
+          },
+          data() {
+            return {
+              label: ref(this.defaultLabel),
+              labelElem: ref(),
+              name: ''
+            }
+          },
+          computed: {
+            loremIpsum() {
+              return this.name + ' lorem ipsum'
+            }
+          }
+        }
+      </script>
+      `
+    },
+    {
+      filename: 'template-ref-function.vue',
+      code: `
+      <template>
+        <button :ref="ref => button = ref">Content</button>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const button = ref();
+        </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'single-ref.vue',
+      code: `
+      <template>
+          <div ref="root"/>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const root = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 7,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'one-ref-unused-in-script.vue',
+      code: `
+      <template>
+          <button ref="button">Content</button>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const buttonRef = ref();
+        const link = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 9,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'multiple-refs.vue',
+      code: `
+      <template>
+          <h1 ref="heading">Heading</h1>
+          <a href="" ref="link">Link</a>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        const heading = ref();
+        const link = ref();
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 8,
+          column: 25
+        },
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 9,
+          column: 22
+        }
+      ]
+    },
+    {
+      filename: 'ref-in-block.vue',
+      code: `
+      <template>
+          <div>
+            <ul>
+              <li ref="firstListItem">Morning</li>
+              <li>Afternoon</li>
+              <li>Evening</li>
+            </ul>
+          </div>
+      </template>
+      <script setup>
+        import { ref } from 'vue';
+        function getFirstListItemElement() {
+          const firstListItem = ref();
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 14,
+          column: 33
+        }
+      ]
+    },
+    {
+      filename: 'setup-function-only-refs.vue',
+      code: `
+      <template>
+        <p>Button clicked {{counter}} times.</p>
+        <button ref="button">Click</button>
+      </template>
+      <script>
+        import { ref } from 'vue';
+        export default {
+          name: 'Counter',
+          setup() {
+            const counter = ref(0);
+            const button = ref();
+          }
+        }
+      </script>
+      `,
+      errors: [
+        {
+          messageId: 'preferUseTemplateRef',
+          line: 12,
+          column: 28
+        }
+      ]
+    }
+  ]
+})